Reverse linked list II [Iterative, Recursion]

Time: O(N); Space: O(1); medium

Reverse a linked list from position m to n. Do it in one-pass.

Constraints:

  • 1 ≤ m ≤ n ≤ length of list.

Example 1:

Input: head = {ListNode} 1->2->3->4->5->None, m = 2, n = 4

Output: {ListNode} 1->4->3->2->5->None

Example 2:

Input: head = {ListNode} 1->2->3->4->None, m = 2, n = 3

Output: {ListNode} 1->3->2->4->None

[9]:
class ListNode(object):
    def __init__(self, x):
        self.val = x
        self.next = None

    def __repr__(self):
        if self:
            return "{} -> {}".format(self.val, repr(self.next))

2. Recursion

Intuition

The idea for linked list reversal using recursion springs from a similar idea that we use for reversing an array. If we want to reverse an array, the huge advantage that we have is the availability of indexes. So, what we can do there is to simply have two pointers, one at the beginning of the array and one at the end. We repeatedly swap elements pointed to by these two pointers and we move both the pointers towards the center of the array. Let’s quickly look at this simple algorithm on a sample array before we move on to linked lists.

The first approach for reversing a portion of the given linked list is based on the similar idea expressed above. Essentially, we want two different pointers, one at the m-th node from the beginning and another one from the n-th node from the beginning. Once we have such pointers in place, we can repeatedly swap the data between the nodes and progress these pointers towards each other like we saw in the case of an array.

However, we don’t have any backward pointers in our linked list and neither do we have any indexes. So, we rely on recursion to simulate the backward pointer. Essentially, the backtracking process in a recursion will help us in simulating the backward movement of the pointer from the n-th node in the linked list towards the center.

Algorithm

  1. We define a recursion function that will do the job of reversing a portion of the linked list.

  2. Let’s call this function recurse. The function takes in 3 parameters: m being the starting point of the reversal, n being the ending point for the reversal, and a pointer right which will start at the n-th node in the linked list and move backwards with the backtracking of the recursion. If this is not clear at the moment, the diagrams that follow will help.

  3. Additionally, we have a pointer called left which starts from the m-th node in the linked list and moves forward. In Python, we have to take a global variable for this which get’s changed with recursion. In other languages, where changes made in function calls persist, we can consider this pointer as an additional variable for the function recurse.

  4. In a recursion call, given m, n, and right, we check if n == 1. If this is the case, we don’t need to go any further.

  5. Until we reach n = 1, we keep moving the right pointer one step forward and after doing that, we make a recursive call with the value of n decreased by 1. At the same time, we keep on moving the left pointer forward until m == 1. When we refer to a pointer being moved forward, it essentially means pointer.next.

  6. So we backtrack as soon as n reaches 1. At that point of time, the right pointer is at the last node of the sublist we want to reverse and the left has already reached the first node of this sublist. So, we swap out the data and move the left pointer one step forward using left = left.next. We need this change to persist across the backtracking process.

  7. From there on, every time we backtrack, the right pointer moves one step backwards. This is the simulation we’ve been mentioning all along. The backward movement is simulated by backtracking.

  8. We stop the swaps when either right == left, which happens if the sublist size is odd, or, right.next == left which happens when during the backtracking process for an even sized sublist, the right pointer crosses left. We use a global boolean flag for stopping the swaps once these conditions are met.

Let’s look at a series of diagrams explaining the process on a sample linked list. Hopefully, things would be clearer after this.

This is the first step in the recursion process. We have a list given to us and the left and the right pointers start off from the head of the linked list. The first step makes a recursive call with updated values of m and n i.e. their values each reduced by 1. Also, the left and the right pointers move one step forward in the linked list.

The next two steps show the movement of the left and the right pointers in the list. Notice that after the second step, the left pointer reaches it’s designated spot. So, we don’t move it any further. Only the right pointer progresses from here on out until it reaches node 6.

As we can see, after the step 5, both the pointers are in their designated spots in the list and we can start the backtracking process. We don’t recurse further. The operation performed during the backtracking is swapping of data between the left and right nodes.

The right pointer crosses the left pointer after step 3 (backtracking) as can be seen above and by that point, we have already reversed the required portion of the linked list. We needed the output list to be [7 → 9 → 8 → 1 → 10 → 2 → 6] and that’s what we have. So, we don’t perform any more swaps and in the code, we can use a global boolean flag to stop the swapping after a point. We can’t really break out of recursion per say.

[12]:
class Solution2(object):
    """
    Time: O(N) since we process all the nodes at-most twice.
          Once during the normal recursion process and once during the backtracking process.
          During the backtracking process we only just swap half of the list if you think about it,
          but the overall complexity is O(N).
    Space: O(N) in the worst case when we have to reverse the entire list.
          This is the space occupied by the recursion stack.
    """
    def reverseBetween(self, head, m, n):
        """
        :type head: ListNode
        :type m: int
        :type n: int
        :rtype: ListNode
        """
        if not head:
            return None

        left, right = head, head
        stop = False
        def recurseAndReverse(right, m, n):
            nonlocal left, stop

            # base case. Don't proceed any further
            if n == 1:
                return

            # Keep moving the right pointer one step forward until (n == 1)
            right = right.next

            # Keep moving left pointer to the right until we reach the proper node
            # from where the reversal is to start.
            if m > 1:
                left = left.next

            # Recurse with m and n reduced.
            recurseAndReverse(right, m - 1, n - 1)

            # In case both the pointers cross each other or become equal, we
            # stop i.e. don't swap data any further. We are done reversing at this
            # point.
            if left == right or right.next == left:
                stop = True

            # Until the boolean stop is false, swap data between the two pointers
            if not stop:
                left.val, right.val = right.val, left.val

                # Move left one step to the right.
                # The right pointer moves one step back via backtracking.
                left = left.next

        recurseAndReverse(right, m, n)
        return head
[13]:
s = Solution2()

head = ListNode(1)
head.next = ListNode(2)
head.next.next = ListNode(3)
head.next.next.next = ListNode(4)
head.next.next.next.next = ListNode(5)
m = 2
n = 4
res = s.reverseBetween(head, m, n)
exp = [1,4,3,2,5]
for val in exp:
    assert res.val == val
    res = res.next

head = ListNode(1)
head.next = ListNode(2)
head.next.next = ListNode(3)
head.next.next.next = ListNode(4)
m = 2
n = 3
res = s.reverseBetween(head, m, n)
exp = [1,3,2,4]
for val in exp:
    assert res.val == val
    res = res.next